use std::{collections::HashMap, env};

use anyhow::{Result, anyhow};
use nanoid::nanoid;
use reqwest::Client;
use serde::{Deserialize, Serialize, de::DeserializeOwned};

use crate::models::{self, AriaParams, Detail, Task};

#[derive(Debug, Clone)]
pub struct AriaRpc {
    token: String,
    aria2_url: String,
    download_dir: String,
    client: Client,
}

#[derive(Debug, Serialize)]
struct RpcReq {
    id: String,
    method: String,
    params: Vec<Param>,
}

#[derive(Debug, Serialize)]
#[serde(untagged)]
enum Param {
    Token(String),
    Uris(Vec<String>),
    Options(HashMap<String, String>),
    Torrent(String),
    Gid(String),
}

#[derive(Debug, Deserialize)]
struct RpcResp<T> {
    error: Option<RpcError>,
    result: Option<T>,
}

#[derive(Debug, Deserialize)]
struct RpcError {
    code: i32,
    message: String,
}

#[derive(Debug, Deserialize)]
struct Version {
    version: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct StatusInfo {
    bittorrent: Option<Bittorrent>,
    completed_length: String,
    connections: String,
    dir: String,
    download_speed: String,
    error_code: Option<String>,
    files: Vec<File>,
    followed_by: Option<Vec<String>>,
    following: Option<String>,
    gid: String,
    seeder: Option<String>,
    status: String,
    total_length: String,
    upload_length: String,
    upload_speed: String,
}

#[derive(Debug, Deserialize)]
struct Bittorrent {
    info: Option<BtInfo>,
    mode: Option<String>,
}

#[derive(Debug, Deserialize)]
struct BtInfo {
    name: String,
}

#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct File {
    completed_length: String,
    index: String,
    length: String,
    path: String,
    selected: String,
}

impl AriaRpc {
    pub fn new() -> Self {
        let token = env::var("TOKEN").unwrap_or("asdfg".into());
        let url = env::var("ARIA2_URL").unwrap_or("127.0.0.1:6800".into());
        let download_dir = env::var("DOWNLOAD_DIR").unwrap_or("~/Downloads".into());
        Self {
            token: format!("token:{}", token),
            aria2_url: format!("http://{}/jsonrpc", url),
            download_dir,
            client: Client::new(),
        }
    }

    async fn send_request<T>(&self, req: &RpcReq) -> Result<T>
    where
        T: DeserializeOwned,
    {
        let resp: RpcResp<T> = self
            .client
            .post(&self.aria2_url)
            .json(&req)
            .send()
            .await?
            .json()
            .await?;

        if let Some(error) = resp.error {
            return Err(anyhow!(
                "aria2 call error code: {}, message: {}",
                error.code,
                error.message
            ));
        }

        resp.result.ok_or(anyhow!("result is None"))
    }

    pub async fn get_version(&self) -> Result<String> {
        let req = RpcReq {
            id: nanoid!(),
            method: "aria2.getVersion".into(),
            params: vec![Param::Token(self.token.clone())],
        };

        let res: Version = self.send_request(&req).await?;

        Ok(res.version)
    }

    pub async fn add_uri(&self, p: AriaParams) -> Result<String> {
        let req = RpcReq {
            id: nanoid!(),
            method: "aria2.addUri".into(),
            params: vec![
                Param::Token(self.token.clone()),
                Param::Uris(p.get_uris()),
                Param::Options(p.file_options(self.download_dir.clone())),
            ],
        };

        let gid: String = self.send_request(&req).await?;

        Ok(gid)
    }

    pub async fn add_magnet(&self, p: AriaParams) -> Result<String> {
        let req = RpcReq {
            id: nanoid!(),
            method: "aria2.addUri".into(),
            params: vec![
                Param::Token(self.token.clone()),
                Param::Uris(p.get_uris()),
                Param::Options(p.magnet_options(self.download_dir.clone())),
            ],
        };

        let gid: String = self.send_request(&req).await?;

        Ok(gid)
    }

    pub async fn add_torrent(&self, p: AriaParams) -> Result<String> {
        let req = RpcReq {
            id: nanoid!(),
            method: "aria2.addTorrent".into(),
            params: vec![
                Param::Token(self.token.clone()),
                Param::Torrent(p.uri.clone()),
                Param::Uris(vec![]),
                Param::Options(p.torrent_options(self.download_dir.clone())),
            ],
        };

        let gid: String = self.send_request(&req).await?;

        Ok(gid)
    }

    pub async fn select_file(&self, p: AriaParams) -> Result<String> {
        let req = RpcReq {
            id: nanoid!(),
            method: "aria2.changeOption".into(),
            params: vec![
                Param::Token(self.token.clone()),
                Param::Gid(p.gid.clone()),
                Param::Options(p.select_options()),
            ],
        };

        self.send_request(&req).await
    }

    pub async fn tell_status(&self, gid: String) -> Result<Task> {
        let req = RpcReq {
            id: nanoid!(),
            method: "aria2.tellStatus".into(),
            params: vec![Param::Token(self.token.clone()), Param::Gid(gid)],
        };

        let info: StatusInfo = self.send_request(&req).await?;

        Ok(info.into())
    }

    pub async fn pause(&self, gid: String) -> Result<String> {
        let req = RpcReq {
            id: nanoid!(),
            method: "aria2.pause".into(),
            params: vec![Param::Token(self.token.clone()), Param::Gid(gid)],
        };

        self.send_request(&req).await
    }

    pub async fn unpause(&self, gid: String) -> Result<String> {
        let req = RpcReq {
            id: nanoid!(),
            method: "aria2.unpause".into(),
            params: vec![Param::Token(self.token.clone()), Param::Gid(gid)],
        };

        self.send_request(&req).await
    }

    pub async fn force_remove(&self, gid: String) -> Result<String> {
        let req = RpcReq {
            id: nanoid!(),
            method: "aria2.forceRemove".into(),
            params: vec![Param::Token(self.token.clone()), Param::Gid(gid)],
        };

        self.send_request(&req).await
    }
}

impl From<StatusInfo> for Task {
    fn from(info: StatusInfo) -> Self {
        let mut t = Task {
            gid: info.gid,
            status: info.status.into(),
            detail_data: Detail {
                dir: info.dir,
                files: info.files.into_iter().map(|f| f.into()).collect(),
                connections: info.connections.parse().unwrap(),
                download_speed: info.download_speed.parse().unwrap(),
                total_length: info.total_length.parse().unwrap(),
                completed_length: info.completed_length.parse().unwrap(),
                upload_speed: info.upload_speed.parse().unwrap(),
                upload_length: info.upload_length.parse().unwrap(),
                followed_by: info.followed_by,
                following: info.following,
                error_code: info.error_code,
                seeder: info.seeder.map(|seeder| seeder.parse().unwrap()),
                ..Default::default()
            },
            ..Default::default()
        };

        if let Some(Bittorrent { info, mode }) = info.bittorrent {
            t.detail_data.mode = mode;
            if let Some(BtInfo { name }) = info {
                t.name = name;
            }
            if t.name.is_empty() {
                t.name = t
                    .detail_data
                    .files
                    .first()
                    .map(|f| f.path.to_string())
                    .unwrap_or_default();
            }
        }

        t
    }
}

impl From<File> for models::File {
    fn from(f: File) -> Self {
        Self {
            index: f.index.parse().unwrap(),
            path: f.path,
            completed_length: f.completed_length.parse().unwrap(),
            length: f.length.parse().unwrap(),
            selected: f.selected.parse().unwrap(),
        }
    }
}
